/**
 * Copyright (C) 2011 ImageLoader for Android Project
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.ryan.aii;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import java.io.*;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.ResourceBundle;

/**
 * User: Ryan
 * Date: 11-6-4
 * Time: 上午10:11
 * Android Image Loader Main Class .
 * <p/>
 * ImageLoader will be load image from remote or local .
 * To use this method , you must has two permission in you AndroidManifest.xml,
 * you just need copy under two lines in your AndroidManifest.xml
 * <uses-permission android:name="android.permission.INTERNET"/>
 * <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 * <p/>
 * if the local already exist and the image is not a expired file( please set EXPIRED_TIME for image),
 * the loader will load from local, else the loader will be delete local file and load from remote.
 * <p/>
 * NOTE : the call back method will be call in UI thread , but the first ,
 * you must call  ImageLoader.load in UI thread
 * WARN : the static method ImageLoader.load must call from UI thread .
 */
public abstract class ImageLoader {

    private  static String IMAGE_SAVE_PATH = "/sdcard/.IMAGE_LOADER/";
    private  static int EXPIRED_TIME = 0x19bfcc00; //the mini seconds for five days.

    static {
        ResourceBundle config = ResourceBundle.getBundle("com/ryan/aii/config");
        IMAGE_SAVE_PATH = config.getString("image_save_path");
        EXPIRED_TIME = Integer.parseInt(config.getString("expired_time"));
        File file = new File(IMAGE_SAVE_PATH);
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    private final static HashMap<String, HashSet<Param>> fac = new HashMap<String, HashSet<Param>>();

    /**
     * load image from local or remote, you must call in UI thread.
     * if the local already exist and the image is not a expired file( please set EXPIRED_TIME for image),
     * the loader will load from local, else the loader will be delete local file and load from remote.
     *
     * @param url      the image url for load.
     * @param data     this data will be in callback method . it just given to callback
     * @param callback Callback object , it will be call in UI thread.
     */
    public static void load(String url, Object data, Callback callback) {
        if (url == null) throw new NullPointerException("image loader url is null.");
        String filepath = getFilePath(url);
        String md5Value = filepath.substring(filepath.lastIndexOf('/') + 1);
        if (null != fac.get(md5Value)) {
            fac.get(md5Value).add(new Param(data, callback));
            return;
        } else {
            HashSet<Param> hsp = new HashSet<Param>();
            hsp.add(new Param(data, callback));
            fac.put(md5Value, hsp);
        }
        new ExeThread(filepath, url, md5Value).start();
    }

    /**
     * new thread to load image.
     */
    private static class ExeThread extends Thread {
        private String filepath;
        private String url;
        private String md5Value;

        ExeThread(String filepath, String url, String md5Value) {
            this.filepath = filepath;
            this.url = url;
            this.md5Value = md5Value;
        }

        public void run() {
            String result = checkFromLocal(filepath);
            if (result != null) {
                onLoad(result);
            } else {
                result = download(filepath, url);
                if (null != result) {
                    onLoad(result);
                }
            }
            fac.remove(md5Value);
        }

        /**
         * download image from remote
         *
         * @param filepath it will be store path in local
         * @param url      can access image url
         * @return image path in local. if failure it is null
         */
        private String download(String filepath, String url) {
            InputStream is = null;
            BufferedInputStream bis = null;
            OutputStream os = null;
            BufferedOutputStream bos = null;
            try {
                File file = new File(filepath);
                if (file.exists()) file.delete();
                is = new URL(url).openStream();
                bis = new BufferedInputStream(is);
                os = new FileOutputStream(file);
                bos = new BufferedOutputStream(os);
                int d;
                while (-1 != (d = bis.read())) {
                    bos.write(d);
                }
                bos.flush();
                return filepath;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (null != is) is.close();
                    if (null != bis) bis.close();
                    if (null != os) os.close();
                    if (null != bos) bos.close();
                } catch (IOException e) {
                    //
                }
            }
            return null;
        }

        /**
         * after load image
         *
         * @param filepath image file path
         */
        private void onLoad(String filepath) {
            HashSet<Param> params = fac.get(md5Value);
            if (null != params) {
                for (Param param : params) {
                    Handler handler = param.callback.getHandler();
                    Message msg = handler.obtainMessage(Callback.IMAGE_LOADER_CALLBACK, param.data);
                    Bundle bundle = new Bundle();
                    bundle.putString(Callback.FILE_NAME, filepath);
                    msg.setData(bundle);
                    handler.sendMessage(msg);
                }
            }
        }

        /**
         * to check file in local, if it exist and it is not expire time
         * the result is filepath , else it will be delete and return null for result
         *
         * @param filepath filepath
         * @return file path or null
         */
        private String checkFromLocal(String filepath) {
            File file = new File(filepath);
            if (!file.exists()) {
                return null;
            } else {
                long lastTime = file.lastModified();
                if ((System.currentTimeMillis() - lastTime) > EXPIRED_TIME) {
                    file.delete();
                    return null;
                } else {
                    return filepath;
                }
            }
        }
    }

    /**
     * Param for load more same url
     */
    private static class Param {
        private Param(Object data, Callback callback) {
            this.data = data;
            this.callback = callback;
        }

        Object data;
        Callback callback;
    }

    /**
     * to get filepath
     *
     * @param url url
     * @return file path
     */
    private static String getFilePath(String url) {
        return new StringBuffer().append(IMAGE_SAVE_PATH).append(md5(url)).toString();
    }

    /**
     * md5 method
     *
     * @param src src
     * @return result
     */
    private static String md5(String src) {
        try {
            int i;
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(src.getBytes());
            byte b[] = md.digest();
            StringBuffer buf = new StringBuffer();
            for (byte aB : b) {
                i = aB;
                if (i < 0) i += 256;
                if (i < 16)
                    buf.append("0");
                buf.append(Integer.toHexString(i));
            }
            return buf.toString();
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    /**
     * call back class
     * it must be instance in UI thread and it will be callback in UI thread.
     */
    public abstract static class Callback {
        public static final String FILE_NAME = "filename";
        public static final int IMAGE_LOADER_CALLBACK = 49804;

        public Handler getHandler() {
            return uiHandler;
        }

        private Handler uiHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                Object data = msg.obj;
                Bundle bundle = msg.getData();
                String filepath = bundle.getString(FILE_NAME);
                onLoad(filepath, data);
            }
        };

        /**
         * the method will callback in UI thread. and you must be instance the class in UI thread.
         *
         * @param filepath filepath in local
         * @param data     this data is you call ImageLoader.load method given.
         */
        public abstract void onLoad(String filepath, Object data);
    }
}
